﻿Imports WPFGlue.Framework

<Assembly: System.Windows.Markup.XmlnsDefinition("http://wpfglue.wordpress.com/navigation", "WPFGlue.Navigation")> 
<Assembly: System.Windows.Markup.XmlnsPrefix("http://wpfglue.wordpress.com/navigation", "n")> 

Namespace Navigation
    ''' <summary>
    ''' Provides a Sticky Command plus several configuration properties in order to help
    ''' with navigating object models.
    ''' </summary>
    ''' <remarks>
    ''' The Navigation class supports the following behaviours:
    ''' <list>
    ''' <item>defining a <c cref="RoutedCommand">RoutedCommand</c> for navigation, which can be used by menus,
    ''' buttons and hyperlinks in order to navigate to pages. While doing that, the DataContext
    ''' of the new page will automatically be set to the object that is passed as CommandParameter</item>
    ''' <item>Restoring the DataContext of a page during back and forward navigation without keeping the 
    ''' page alive. See the <c cref="Session">Session</c> object for details.</item>
    ''' </list>
    ''' <item>blocking all navigation outside a page through setting the 
    ''' <c cref="Navigation.BlockNavigationProperty">BlockNavigation</c> attached property to true.</item>
    ''' </remarks>
    Public Class Navigation
        Inherits System.Windows.DependencyObject

        Private Const SessionKey As String = "WPFGlue.Navigation.Session"

#Region "Navigation Command"
        ''' <summary>
        ''' The RoutedCommand that is used to invoke the Navigation behaviour.
        ''' </summary>
        ''' <remarks>
        ''' Navigation is controlled through this command. Setting this property to
        ''' Nothing disables Navigation support.
        ''' </remarks>
        Public Shared ReadOnly CommandProperty As DependencyProperty = DependencyProperty.RegisterAttached("Command", GetType(RoutedCommand), GetType(Navigation), New PropertyMetadata(AddressOf OnCommandChanged))
        Public Shared Function GetCommand(ByVal d As DependencyObject) As RoutedCommand
            Return d.GetValue(CommandProperty)
        End Function
        Public Shared Sub SetCommand(ByVal d As DependencyObject, ByVal value As RoutedCommand)
            d.SetValue(CommandProperty, value)
        End Sub
        Private Shared Sub OnCommandChanged(ByVal d As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
            If e.OldValue IsNot Nothing Then
                Detach(d, e.OldValue)
            End If
            If e.NewValue IsNot Nothing Then
                Attach(d, e.NewValue)
            End If
        End Sub
        Private Shared Sub OnUnloaded(ByVal sender As Object, ByVal e As RoutedEventArgs)
            SetCommand(sender, Nothing)
        End Sub
        Private Shared Sub Attach(ByVal d As DependencyObject, ByVal command As RoutedCommand)
            If d IsNot Nothing Then
                Dim service As NavigationService = GetNavigationService(d)
                If service IsNot Nothing Then
                    AddHandler service.Navigating, AddressOf BlockNavigationHandler
                    AddHandler service.NavigationFailed, AddressOf CancelNavigationHandler
                    AddHandler service.NavigationStopped, AddressOf CancelNavigationHandler
                End If
                StickyComponentManager.Attach(d, command, AddressOf ExecuteGotoPage, AddressOf CanExecuteGotoPage)
                StickyComponentManager.Attach(d, FrameworkElement.UnloadedEvent, AddressOf OnUnloaded)
            End If
        End Sub
        Public Shared Sub Detach(ByVal d As DependencyObject, ByVal command As RoutedCommand)
            If d IsNot Nothing Then
                Dim service As NavigationService = GetNavigationService(d)
                If service IsNot Nothing Then
                    RemoveHandler service.Navigating, AddressOf BlockNavigationHandler
                    RemoveHandler service.NavigationFailed, AddressOf CancelNavigationHandler
                    RemoveHandler service.NavigationStopped, AddressOf CancelNavigationHandler
                End If
                StickyComponentManager.Detach(d, command)
                StickyComponentManager.Detach(d, FrameworkElement.UnloadedEvent, AddressOf OnUnloaded)
            End If
        End Sub

        Private Shared Sub ExecuteGotoPage(ByVal sender As Object, ByVal e As ExecutedRoutedEventArgs)
            If CanNavigate(e.Source, e.OriginalSource, e.Parameter) Then
                Dim uri As Uri = GetNavigationUri(e.OriginalSource)
                Dim param As Object = e.Parameter
                Navigate(sender, uri, param)
                e.Handled = True
            End If
        End Sub
        Private Shared Sub CanExecuteGotoPage(ByVal sender As Object, ByVal e As System.Windows.Input.CanExecuteRoutedEventArgs)
            e.CanExecute = CanNavigate(e.Source, e.OriginalSource, e.Parameter)
            e.ContinueRouting = False
            e.Handled = True
        End Sub
        Private Shared Function CanNavigate(ByVal source As DependencyObject, ByVal originalSource As DependencyObject, ByVal parameter As Object) As Boolean
            Dim result As Boolean = True
            result = result And Not GetBlockNavigation(source)
            result = result And GetCanNavigate(originalSource)
            result = result And GetNavigationUri(originalSource) IsNot Nothing
            Return result
        End Function
#End Region

#Region "Configuration Properties"
        ''' <summary>
        ''' Contains the URI a commandsource will navigate to.
        ''' </summary>
        ''' <remarks>
        ''' If the CommandSource is a <c cref="Hyperlink">Hyperlink</c>, the NavigateUri of
        ''' the Hyperlink is used as fallback if Navigation.Uri is not set.
        ''' </remarks>
        Public Shared ReadOnly UriProperty As DependencyProperty = DependencyProperty.RegisterAttached("Uri", GetType(Uri), GetType(Navigation))
        Public Shared Function GetUri(ByVal d As DependencyObject) As Uri
            Return d.GetValue(UriProperty)
        End Function
        Public Shared Sub SetUri(ByVal d As DependencyObject, ByVal value As Uri)
            d.SetValue(UriProperty, value)
        End Sub

        ''' <summary>
        ''' Enables or disables the navigation command for the element it is set on.
        ''' </summary>
        ''' <remarks>
        ''' The default value is true. If set to false, the navigation command will
        ''' be disabled for this commandsource. Use this property to control navigation
        ''' selectively through triggers, styles or data binding.
        ''' </remarks>
        Public Shared ReadOnly CanNavigateProperty As DependencyProperty = DependencyProperty.RegisterAttached("CanNavigate", GetType(Boolean), GetType(Navigation), New PropertyMetadata(True))
        Public Shared Function GetCanNavigate(ByVal d As DependencyObject) As Boolean
            Return d.GetValue(CanNavigateProperty)
        End Function
        Public Shared Sub SetCanNavigate(ByVal d As DependencyObject, ByVal value As Boolean)
            d.SetValue(CanNavigateProperty, value)
        End Sub

        ''' <summary>
        ''' Blocks navigation for the whole page.
        ''' </summary>
        ''' <remarks>
        ''' If set to true, prohibits the user to navigate away from the page. 
        ''' </remarks>
        Public Shared ReadOnly BlockNavigationProperty As DependencyProperty = DependencyProperty.RegisterAttached("BlockNavigation", GetType(Boolean), GetType(Navigation), New FrameworkPropertyMetadata(False, FrameworkPropertyMetadataOptions.Inherits, Nothing, AddressOf CoerceBlockNavigation))
        Public Shared Function GetBlockNavigation(ByVal d As DependencyObject) As Boolean
            Return d.GetValue(BlockNavigationProperty)
        End Function
        Public Shared Sub SetBlockNavigation(ByVal d As DependencyObject, ByVal value As Boolean)
            d.SetValue(BlockNavigationProperty, value)
        End Sub
        Private Shared Function CoerceBlockNavigation(ByVal d As DependencyObject, ByVal baseValue As Object) As Object
            If GetIsExpired(d) Then
                Return False
            Else
                Return baseValue
            End If
        End Function
#End Region

#Region "Expired Content Handling"
        Public Shared ReadOnly IsExpiredProperty As DependencyProperty = DependencyProperty.RegisterAttached("IsExpired", GetType(Boolean), GetType(Navigation), New FrameworkPropertyMetadata(False, FrameworkPropertyMetadataOptions.Journal, AddressOf OnIsExpiredChanged))
        Public Shared Function GetIsExpired(ByVal d As DependencyObject) As Boolean
            Return d.GetValue(IsExpiredProperty)
        End Function
        Public Shared Sub SetIsExpired(ByVal d As DependencyObject, ByVal value As Boolean)
            d.SetValue(IsExpiredProperty, value)
        End Sub
        Private Shared Sub OnIsExpiredChanged(ByVal d As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
            If e.OldValue Then
                d.SetValue(ExpiredAdornerProperty, Nothing)
            End If
            If e.NewValue Then
                d.SetValue(ExpiredAdornerProperty, New ExpiredPage(d))
            End If
            d.CoerceValue(BlockNavigationProperty)
        End Sub

        Private Shared ReadOnly ExpiredAdornerProperty As DependencyProperty = DependencyProperty.RegisterAttached("ExpiredAdorner", GetType(Adorner), GetType(Navigation), New PropertyMetadata(AddressOf StickyComponentManager.OnStickyComponentChanged))


        Public Shared ReadOnly ShowExpiredPageProperty As DependencyProperty = DependencyProperty.RegisterAttached("ShowExpiredPage", GetType(Boolean), GetType(Navigation))
        Public Shared Function GetShowExpiredPage(ByVal d As DependencyObject) As Boolean
            Return d.GetValue(ShowExpiredPageProperty)
        End Function
        Public Shared Sub SetShowExpiredPage(ByVal d As DependencyObject, ByVal value As Boolean)
            d.SetValue(ShowExpiredPageProperty, value)
        End Sub
#End Region

#Region "Navigation Flow Control"

        Public Shared Function Navigate(ByVal sender As Object, ByVal target As Uri, ByVal data As Object) As Boolean
            Dim result As Boolean = False
            Dim service As NavigationService = GetNavigationService(sender)
            If service IsNot Nothing Then
                AddHandler service.LoadCompleted, AddressOf SetDataContextHandler
                result = service.Navigate(target, data)
            End If
            Return result
        End Function

        Private Shared Sub SetDataContextHandler(ByVal sender As Object, ByVal e As System.Windows.Navigation.NavigationEventArgs)
            Dim service As NavigationService = GetNavigationService(e.Navigator)
            If service IsNot Nothing Then
                RemoveHandler service.LoadCompleted, AddressOf SetDataContextHandler
                Dim data As Object = e.ExtraData
                If data IsNot Nothing Then
                    If TypeOf e.Content Is DependencyObject Then
                        SetDataContext(e.Content, data)
                        AddHandler service.Navigating, AddressOf SaveDataContextHandler
                    End If
                End If
            End If
        End Sub

        Private Shared Sub SaveDataContextHandler(ByVal sender As Object, ByVal e As System.Windows.Navigation.NavigatingCancelEventArgs)
            Dim service As NavigationService = GetNavigationService(e.Navigator)
            If service IsNot Nothing Then
                RemoveHandler service.Navigating, AddressOf SaveDataContextHandler
                If Not e.Cancel AndAlso TypeOf service.Content Is DependencyObject Then
                    Dim content As DependencyObject = service.Content
                    Dim data As Object = content.GetValue(FrameworkElement.DataContextProperty)
                    If data IsNot Nothing Then
                        Dim id As Integer = GetDataContextId(content)
                        id = Session.StoreReference(id, data)
                        Dim state As NavigationContentState = New NavigationContentState(id, e.ContentStateToSave)
                        e.ContentStateToSave = state
                    End If
                End If
            End If
        End Sub

        Private Shared Sub BlockNavigationHandler(ByVal sender As Object, ByVal e As System.Windows.Navigation.NavigatingCancelEventArgs)
            Dim service As NavigationService = GetNavigationService(sender)
            If service IsNot Nothing Then
                If service.Content IsNot Nothing Then
                    e.Cancel = GetBlockNavigation(service.Content)
                    'Allow Fragment navigation
                    If e.Cancel And Uri.Compare(service.CurrentSource, service.Source, UriComponents.PathAndQuery, UriFormat.UriEscaped, StringComparison.InvariantCulture) = 0 Then
                        e.Cancel = False
                    End If
                End If
            End If
        End Sub

        Private Shared Sub CancelNavigationHandler(ByVal sender As Object, ByVal e As System.EventArgs)
            Dim service As NavigationService = Nothing
            If TypeOf e Is NavigationEventArgs Then
                service = GetNavigationService(DirectCast(e, NavigationEventArgs).Navigator)
            ElseIf TypeOf e Is NavigationFailedEventArgs Then
                service = GetNavigationService(DirectCast(e, NavigationFailedEventArgs).Navigator)
            End If
            If service IsNot Nothing Then
                RemoveHandler service.LoadCompleted, AddressOf SetDataContextHandler
            End If
        End Sub

        <Serializable()> _
        Public Class NavigationContentState
            Inherits CustomContentState
            Private _DataContextId As Integer = -1
            Private _OriginalState As System.Windows.Navigation.CustomContentState

            Public Sub New(ByVal id As Integer, ByVal originalState As CustomContentState)
                MyBase.New()
                If Not TypeOf originalState Is NavigationContentState Then
                    _OriginalState = originalState
                End If
                _DataContextId = id
            End Sub

            Public Overrides Sub Replay(ByVal navigationService As System.Windows.Navigation.NavigationService, ByVal mode As System.Windows.Navigation.NavigationMode)
                If _OriginalState IsNot Nothing Then
                    _OriginalState.Replay(navigationService, mode)
                End If
                If _DataContextId <> -1 Then
                    Dim content As DependencyObject = TryCast(navigationService.Content, DependencyObject)
                    If content IsNot Nothing Then
                        Dim data As Object = Session.RetrieveReference(_DataContextId)
                        If data Is Nothing Then
                            SetIsExpired(content, True)
                            WPFGlue.Validation.Validation.SetSuppressErrorTemplate(content, True)
                        Else
                            SetDataContext(content, data)
                            SetDataContextId(content, _DataContextId)
                            AddHandler navigationService.Navigating, AddressOf SaveDataContextHandler
                        End If
                    End If
                End If
            End Sub
        End Class
#End Region

#Region "DataContext Setter"
        Public Shared ReadOnly DataContextProperty As DependencyProperty = _
            DependencyProperty.RegisterAttached("DataContext", GetType(Object), _
            GetType(Navigation), New PropertyMetadata(Nothing, AddressOf OnDataContextChanged))
        Public Shared Function GetDataContext(ByVal d As DependencyObject) As Object
            Return d.GetValue(DataContextProperty)
        End Function
        Public Shared Sub SetDataContext(ByVal d As DependencyObject, ByVal value As Object)
            d.SetValue(DataContextProperty, value)
        End Sub
        Private Shared Sub OnDataContextChanged(ByVal d As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
            If e.NewValue IsNot Nothing Then
                If StickyComponentManager.GetIsLoaded(d) Then
                    d.Dispatcher.Invoke(System.Delegate.CreateDelegate(GetType(Navigation), Nothing, "OnDataContextLoaded"), d, Nothing)
                Else
                    StickyComponentManager.AttachEvent(d, FrameworkElement.LoadedEvent, AddressOf OnDataContextLoaded)
                End If
            End If
        End Sub
        Private Shared Sub OnDataContextLoaded(ByVal sender As Object, ByVal e As RoutedEventArgs)
            Dim d As DependencyObject = TryCast(sender, DependencyObject)
            If d IsNot Nothing Then
                StickyComponentManager.DetachEvent(d, FrameworkElement.LoadedEvent, AddressOf OnDataContextLoaded)
                Dim data As Object = GetDataContext(d)
                d.SetValue(FrameworkElement.DataContextProperty, data)
                d.ClearValue(DataContextProperty)
            End If
        End Sub

        Private Shared ReadOnly DataContextIdPropertyKey As DependencyPropertyKey = DependencyProperty.RegisterAttachedReadOnly("DataModelId", GetType(Integer), GetType(Navigation), New PropertyMetadata(-1))
        Public Shared ReadOnly DataContextIdProperty As DependencyProperty = DataContextIdPropertyKey.DependencyProperty
        Public Shared Function GetDataContextId(ByVal element As DependencyObject) As Integer
            Return element.GetValue(DataContextIdProperty)
        End Function
        Private Shared Sub SetDataContextId(ByVal element As DependencyObject, ByVal value As Integer)
            element.SetValue(DataContextIdPropertyKey, value)
        End Sub

#End Region

#Region "Utility Functions"
        Public Shared Function GetNavigationService(ByVal d As DependencyObject) As NavigationService
            Dim service As NavigationService = Nothing
            If TypeOf d Is NavigationWindow Then
                service = DirectCast(d, NavigationWindow).NavigationService
            ElseIf TypeOf d Is Frame Then
                service = DirectCast(d, Frame).NavigationService
            ElseIf TypeOf d Is Page Then
                service = DirectCast(d, Page).NavigationService
            Else
                service = NavigationService.GetNavigationService(d)
            End If
            Return service
        End Function

        Public Shared ReadOnly Property Session() As Session
            Get
                Dim result As Session = Application.Current.Properties.Item(SessionKey)
                If result Is Nothing Then
                    result = New Session
                    Application.Current.Properties.Add(SessionKey, result)
                End If
                Return result
            End Get
        End Property

        Public Shared Function GetNavigationUri(ByVal d As DependencyObject) As Uri
            Dim result As Uri = GetUri(d)
            If d Is Nothing AndAlso TypeOf d Is Hyperlink Then
                result = d.GetValue(Hyperlink.NavigateUriProperty)
            End If
            Return result
        End Function
#End Region

    End Class
End Namespace

